
---@class DIYUtility
local DIY = require "packages/diy_utility/_base"

--旨在为diy提供便利，避免每个服务器造一遍轮子的utility。如modder有需要欢迎向本仓库发起PR

-- 调用此仓库内容，需要在引用之前输入 local DIY = require "packages/diy_utility/diy_utility"

--原则上应该兼容原有的功能。e.g.假如你的服务器需要的是“回合结束后再将牌置入弃牌堆的中央区”，而非本utility提供的“牌一直存在于弃牌堆的中央区”

---------------------------------------------------------------------------------------------------
--中央区（本回合进入弃牌堆，且当前存在于弃牌堆的牌）
--使用方法：在相关技能中Room:addSkill("#CenterArea")

--贴有此标记的牌视为不在中央区，用于“移去中央区某些牌”。可带"-phase"、"-turn"等suffix
MarkEnum.RemoveCenterCards = "RemoveCenterCards"

--贴有此标记的弃牌堆中的牌视为在中央区，不会随回合结束清理，用于“将某些牌固定于中央区”。可带"-phase"、"-turn"等suffix
MarkEnum.IncludeCenterCards = "IncludeCenterCards"

--- CenterAreaRemoveData 中央区清除数据
---@class CenterAreaRemoveDataSpec
---@field public ids integer[] @ 中央区清除的牌的id列表
---@field public proposer ServerPlayer @ 执行者

---@class DIY.CenterAreaRemoveData: CenterAreaRemoveDataSpec, TriggerData
DIY.CenterAreaRemoveData = TriggerData:subclass("CenterAreaRemoveData")

--- TriggerEvent
---@class DIY.CenterAreaRemoveDataTriggerEvent: TriggerEvent
---@field public data DIY.CenterAreaRemoveData
DIY.CenterAreaRemoveDataTriggerEvent = TriggerEvent:subclass("CenterAreaRemoveDataEvent")

--- 中央区清除前
---@class DIY.BeforeCenterAreaRemove: DIY.CenterAreaRemoveDataTriggerEvent
DIY.BeforeCenterAreaRemove = DIY.CenterAreaRemoveDataTriggerEvent:subclass("fk.BeforeCenterAreaRemove")
--- 中央区清除后
---@class DIY.AfterCenterAreaRemove: DIY.CenterAreaRemoveDataTriggerEvent
DIY.AfterCenterAreaRemove = DIY.CenterAreaRemoveDataTriggerEvent:subclass("fk.AfterCenterAreaRemove")

---@alias CenterAreaRemoveDataTrigFunc fun(self: TriggerSkill, event: DIY.CenterAreaRemoveDataTriggerEvent,
---  target: ServerPlayer, player: ServerPlayer, data: DIY.CenterAreaRemoveData):any

---@class SkillSkeleton
---@field public addEffect fun(self: SkillSkeleton, key: DIY.CenterAreaRemoveDataTriggerEvent,
---  data: TrigSkelSpec<CenterAreaRemoveDataTrigFunc>, attr: TrigSkelAttribute?): SkillSkeleton

---获取中央区的牌
---@param room Room | AbstractRoom
---@return integer[]
DIY.getCenterCards = function (room)
  return room:getBanner("@$CenterArea") or {}
end

---设置中央区的牌
---@param room Room
---@param cards integer[]
DIY.setCenterCards = function (room, cards)
  room:setBanner("@$CenterArea", #cards > 0 and cards or {})
end

--将一些牌移出中央区（仍在弃牌堆内，但不计入中央区计算）
---@param player ServerPlayer @ 操作的角色
---@param card integer | integer[] | Card | Card[] @ 要移出的牌
---@param scope? integer @ 移出的历史范围，默认当回合
DIY.removeCenterCards = function (player, card, scope)
  local room = player.room
  local banner = DIY.getCenterCards(room)
  if #banner == 0 then return end
  local suffix_mapper = {
    [Player.HistoryGame] = "",
    [Player.HistoryRound] = "-round",
    [Player.HistoryTurn] = "-turn",
    [Player.HistoryPhase] = "-phase",
  }
  scope = scope or Player.HistoryTurn
  local ids = Card:getIdList(card)
  ids = table.filter(ids, function (id)
    return table.contains(banner, id)
  end)
  if #ids > 0 then
    for _, id in ipairs(ids) do
      table.removeOne(banner, id)
      room:setCardMark(Fk:getCardById(id), MarkEnum.RemoveCenterCards..suffix_mapper[scope], 1)
    end
    room.logic:trigger(DIY.BeforeCenterAreaRemove, player, {ids = ids, proposer = player})
    DIY.setCenterCards(room, banner)
    room.logic:trigger(DIY.AfterCenterAreaRemove, player, {ids = ids, proposer = player})
  end
end

---------------------------------------------------------------------------------------------------
--明置牌（将一些手牌对所有角色可见。官方仅国际服国战田豫有此功能）

--贴有此标记的牌在手牌中明置，离开手牌时清除。可带"-phase"、"-turn"等suffix
MarkEnum.ShownCards = "@@ShownCards-inhand"

--明置手牌
---@param player ServerPlayer @ 目标角色
---@param card integer | integer[] | Card | Card[] @ 要明置的牌
---@param scope? integer @ 明置的历史范围，默认永久
DIY.showCards = function (player, card, scope)
  player.room:addSkill("#ShownCards")
  local suffix_mapper = {
    [Player.HistoryGame] = "",
    [Player.HistoryRound] = "-round",
    [Player.HistoryTurn] = "-turn",
    [Player.HistoryPhase] = "-phase",
  }
  scope = scope or Player.HistoryGame
  local ids = Card:getIdList(card)
  ids = table.filter(ids, function (id)
    return table.contains(player:getCardIds("h"), id)
  end)
  if #ids > 0 then
    for _, id in ipairs(ids) do
      player.room:setCardMark(Fk:getCardById(id), MarkEnum.ShownCards..suffix_mapper[scope], 1)
    end
    player:showCards(ids)
  end
end

--暗置手牌
---@param player ServerPlayer @ 目标角色
---@param card integer | integer[] | Card | Card[] @ 要暗置的牌
DIY.hideCards = function (player, card)
  local room = player.room
  local ids = Card:getIdList(card)
  ids = table.filter(ids, function (id)
    return table.contains(player:getCardIds("h"), id)
  end)
  if #ids > 0 then
    for _, id in ipairs(ids) do
      room:setCardMark(Fk:getCardById(id), MarkEnum.ShownCards, 0)
      for _, suffix in ipairs(MarkEnum.TempMarkSuffix) do
        room:setCardMark(Fk:getCardById(id), MarkEnum.ShownCards..suffix, 0)
      end
    end
  end
end

--获得一名角色的明置手牌id列表
---@param player ServerPlayer | Player @ 目标角色
---@return integer[]
DIY.getShownCards = function (player)
  return table.filter(player:getCardIds("h"), function (id)
    if Fk:getCardById(id):getMark(MarkEnum.ShownCards) > 0 then
      return true
    end
    if table.find(MarkEnum.TempMarkSuffix, function (suffix)
      return Fk:getCardById(id):getMark(MarkEnum.ShownCards..suffix) > 0
    end) then
      return true
    end
    return false
  end)
end

---@class AskToCardsParams: AskToUseActiveSkillParams
---@field skip? boolean @ 是否跳过明置，默认不跳过

--- 询问一名玩家明置若干张手牌，返回选择的牌。与本体askToCards类似。
--- 与本体askToCards类似
---@param player ServerPlayer @ 要询问的玩家
---@param params AskToCardsParams @ 各种变量
---@return integer[] @ 选择的牌的id列表，可能是空的
DIY.askToShowCards = function (player, params)
  local room = player.room
  local cancelable = params.cancelable or params.cancelable == nil
  local cards = table.filter(player:getCardIds("h"), function (id)
    return not table.contains(DIY.getShownCards(player), id)
  end)
  if #cards == 0 then return {} end
  local ret = room:askToCards(player, {
    min_num = params.min_num,
    max_num = params.max_num,
    include_equip = false,
    skill_name = params.skill_name,
    pattern = tostring(Exppattern{ id = cards }),
    prompt = params.prompt,
    cancelable = cancelable,
  })
  if not cancelable and #ret == 0 then
    ret = table.random(cards, params.min_num)
  end
  if #ret > 0 and (params.skip or params.skip == nil) then
    DIY.showCards(player, ret)
  end
  return ret
end

---------------------------------------------------------------------------------------------------

--- 进入隐匿状态（身份局的开局隐匿在游戏流程中，此函数适用于diy的主动进入隐匿状态）
---@param player ServerPlayer @ 目标角色
DIY.enterHidden = function (player)
  local room = player.room
  room:sendLog({
    type = "#EnterHidden",
    from = player.id,
  })
  local skills = "hidden_skill&"
  room:setPlayerMark(player, "__hidden_general", player.general)
  for _, s in ipairs(Fk.generals[player.general]:getSkillNameList(true)) do
    if player:hasSkill(s, true) then
      skills = skills.."|-"..s
    end
  end
  if player.deputyGeneral ~= "" then
    room:setPlayerMark(player, "__hidden_deputy", player.deputyGeneral)
    for _, s in ipairs(Fk.generals[player.deputyGeneral]:getSkillNameList(true)) do
      if player:hasSkill(s, true) then
        skills = skills.."|-"..s
      end
    end
  end
  player.general = "hiddenone"
  player.gender = General.Male
  room:broadcastProperty(player, "gender")
  if player.deputyGeneral ~= "" then
    player.deputyGeneral = ""
  end
  player.kingdom = "jin"
  room:setPlayerMark(player, "__hidden_record",
  {
    maxHp = player.maxHp,
    hp = player.hp,
  })
  player.maxHp = 1
  player.hp = 1
  for _, property in ipairs({"general", "deputyGeneral", "kingdom", "maxHp", "hp"}) do
    room:broadcastProperty(player, property)
  end
  room:handleAddLoseSkills(player, skills, nil, false, true)
end
Fk:loadTranslationTable{
  ["#EnterHidden"] = "%from 进入隐匿状态",
  ["enter_hidden_href"] = "记录当前的体力上限和体力值，失去当前武将牌上的所有技能，将武将替换为隐匿将。再次登场后，"..
  "将体力上限和体力值调整为记录值，武将替换回隐匿前的武将，获得武将牌上所有的技能。",
}

---------------------------------------------------------------------------------------------------

--- 将角色移出游戏
--- 复用调虎离山概念。指角色于不计入距离和座次的计算，不能使用牌且不是牌的合法目标，且防止体力值改变。
---@param player ServerPlayer @ 目标角色
---@param temp? string @ 作用时间范围，``-round`` ``-turn`` ``-phase`` 若填其他内容则需调用returnPlayer函数，输入相同的temp参数返回游戏
DIY.removePlayer = function (player, temp)
  local room = player.room
  if not room:hasSkill("#remove_player") then
    room:addSkill("#remove_player")
  end
  if temp == nil then temp = "" end
  room:addPlayerMark(player, MarkEnum.PlayerRemoved..temp, 1)
  if table.contains(MarkEnum.TempMarkSuffix, temp) or temp == "" then
    room:setPlayerMark(player, "@@remove_player"..temp, 1)
  else
    room:addTableMark(player, "@@remove_player_list", temp)
  end
end

--- 将角色返回游戏（解除移出游戏状态）
---@param player ServerPlayer @ 目标角色
---@param temp string @ 移出游戏标识
DIY.returnPlayer = function (player, temp)
  player.room:removeTableMark(player, "@@remove_player_list", temp)
end

Fk:loadTranslationTable{
  ["#remove_player_prohibit"] = "移出游戏",
  ["@@remove_player"] = "移出游戏",
  ["@@remove_player-turn"] = "移出游戏",
  ["@@remove_player-phase"] = "移出游戏",
  ["@@remove_player-round"] = "移出游戏",
  ["@@remove_player_list"] = "移出游戏",
}

---------------------------------------------------------------------------------------------------

--[[
  新系统 蓄势技

  蓄势技使用后进入蓄势中状态，不可使用。
  有蓄势技的角色在出牌阶段空闲时间点，可以弃置三张牌，复原一个本回合未使用的蓄势技。
  洗牌后，所有角色复原已使用的蓄势技。

--]]

--- 蓄势技标记（放在技能骨架skeleton的tags中）
DIY.ReadySkill = "ready_skill"

Fk:loadTranslationTable{
  ["diy_ready_skill"] = "一种技能标签，拥有此标签的技能发动后失效，直到牌堆洗切后重置；出牌阶段，你可以弃置三张牌，"..
  "重置一个本回合未发动过的蓄势技。",
  ["@diy_skill_readying"] = "蓄势中",
}

-- 角色的某技能是否蓄势中
---@param player Player @ 目标角色
---@param skill Skill|string @ 目标技能
---@return boolean
DIY.isReadying = function(player, skill)
  if type(skill) == "string" then
    skill = Fk.skills[skill]
    if skill == nil then return false end
  end
  return skill:getSkeleton() ~= nil and table.contains(player:getTableMark("@diy_skill_readying"), skill:getSkeleton().name)
end

-- 角色的某技能的蓄势进度，即还需要弃置几张牌才能完成蓄势，0为已完成
---@param player Player @ 目标角色
---@param skill Skill|string @ 目标技能
---@return integer
DIY.getReadyProgress = function(player, skill)
  if type(skill) ~= "string" then
    skill = skill.name
  end
  local mark = player:getMark("diy_ready_progress")
  if type(mark) == "table" then
    return mark[skill] or 0
  end
  return 0
end

-- 角色的某技能的蓄势上限，即一共需要弃置几张牌才能完成蓄势，默认为3
---@param player Player @ 目标角色
---@param skill Skill|string @ 目标技能
---@return integer
DIY.getReadyCap = function(player, skill)
  if type(skill) ~= "string" then
    skill = skill.name
  end
  local mark = player:getMark("diy_ready_cost")
  if type(mark) == "table" then
    return mark[skill] or 3
  end
  return 3
end

-- 设置蓄势上限，即一共需要弃置几张牌才能完成蓄势，默认为3
---@param player ServerPlayer @ 目标角色
---@param skill Skill|string @ 目标技能
---@param num? integer @ 设置的数量。不填则还原为3
DIY.setReadyCap = function(player, skill, num)
  if type(skill) ~= "string" then
    skill = skill.name
  end
  local mark = player:getTableMark("diy_ready_cost")
  mark[skill] = (num or 3)
  player.room:setPlayerMark(player, "diy_ready_cost", mark)
end

-- 角色的某技能的蓄势进度，即还需要弃置几张牌才能完成蓄势，0为已完成
---@param player ServerPlayer @ 目标角色
---@param skill Skill|string @ 目标技能
---@param num integer @ 变化值，正数为增加进度
---@param reason string @ 加减的原因，一般为技能名
---@return boolean @ 若完成蓄势，返回真
DIY.changeReadyProgress = function(player, skill, num, reason)
  if type(skill) ~= "string" then
    skill = skill.name
  end
  if not DIY.isReadying(player, skill) then return false end
  local mark = player:getTableMark("diy_ready_progress")
  if mark[skill] == nil then
    mark[skill] = 0
  end
  mark[skill] = math.max(mark[skill] + num, 0)
  player.room:setPlayerMark(player, "diy_ready_progress", mark)
  if mark[skill] >= DIY.getReadyCap(player, skill) then
    DIY.resetReadySkill(player, skill, reason)
    return true
  end
  return false
end


-- 令角色的蓄势技进入蓄势中
---@param player ServerPlayer @ 目标角色
---@param skill Skill|string @ 目标技能
---@param reason? string|"useskill" @ 切换原因（默认为"useskill"）
---@param no_trigger? boolean @ 是否不触发事件。默认触发
DIY.setReadySkill = function(player, skill, reason, no_trigger)
  if reason == nil then reason = "useskill" end
  if type(skill) == "string" then
    skill = Fk.skills[skill]
    if skill == nil then return end
  end
  if not skill:hasTag(DIY.ReadySkill) then return end
  player.room:addTableMark(player, "@diy_skill_readying", skill:getSkeleton().name)
  if not no_trigger then
    player.room.logic:trigger(DIY.SkillReadyStart, player, {
      who = player, skill = Fk.skills[skill:getSkeleton().name], reason = reason
    })
  end
end

-- 重置角色的蓄势技
---@param player ServerPlayer @ 目标角色
---@param skill Skill|string @ 目标技能
---@param reason string|"discard"|"shuffle" @ 重置原因（"discard"为因弃牌重置；"shuffle"为因洗牌重置）
---@param no_trigger? boolean @ 是否不触发事件。默认触发
---@param cards? integer[] @ 弃置的牌，可能无
DIY.resetReadySkill = function(player, skill, reason, no_trigger, cards)
  assert(reason)
  if type(skill) == "string" then
    skill = Fk.skills[skill]
    if skill == nil then return end
  end
  if not DIY.isReadying(player, skill) then return end
  local record = player:getTableMark("diy_ready_progress")
  if record[skill.name] then
    record[skill.name] = 0
    player.room:setPlayerMark(player, "diy_ready_progress", record)
  end
  player.room:removeTableMark(player, "@diy_skill_readying", skill:getSkeleton().name)
  if not no_trigger then
    player.room.logic:trigger(DIY.SkillReadyFinish, player, {
      who = player, skill = Fk.skills[skill:getSkeleton().name], reason = reason, cards = cards
    })
  end
end


--[[
  多项转换技：项数大于2的转换技
  用名为diy_switch的角色表型标记储存所有这类转换技
  标记格式为{技能名，当前项数，总项数，标记颜色}
  请在多项转换技的on_aquire中使用DIY.setSwitchState(player, skillName, 1, total)用于新建转换技
  DIY.changeSwitchState(player, skillName, add)用于增加或减少当前项数
  若从最后一项返回第一项，触发fk.diySwitchLoopback（周转）事件
  注意，请务必在技能on_lose调用removeSwitchSkill清除标记占用

  标记命名格式：
  @!diy_ .. 颜色 .. 项数(1~9)，如 @!diy_red1
  大于等于10项时，标记命名为 @!diy_redx ，使用标记数量代表当前项
  注意，UI上标记数至多显示为999
--]]


--- 更新角色转换技的UI标记。
--- @param player ServerPlayer @ 目标角色
--- @param color string @ 标记颜色
--- @param old? integer @ 旧项数
--- @param new? integer @ 新项数
local refreshSwitchMark = function (player, color, old, new)
  if old then
    if old < 10 then
      player.room:setPlayerMark(player, "@!diy_"..color..old, 0)
    else
      player.room:setPlayerMark(player, "@!diy_"..color.."x", 0)
    end
  end
  if new then
    if new < 10 then
      player.room:setPlayerMark(player, "@!diy_"..color..new, 1)
    else
      player.room:setPlayerMark(player, "@!diy_"..color.."x", new)
    end
  end
end

--- 将多项转换技的当前项数增加1项
---@param player ServerPlayer @ 目标角色
---@param skillName string @ 转换技总项数
---@param add? boolean @ 是否为增加，写false则后退一项。默认增加
DIY.changeSwitchState = function (player, skillName, add)
  if player.dead then return end
  local room = player.room
  local mark = player:getTableMark("diy_switch")
  local skillIndex
  for i, v in ipairs(mark) do
    if v[1] == skillName then
      skillIndex = i
      break
    end
  end
  if skillIndex == nil then
    p("Warning:Cannot find the index of the switch skill ["..skillName.."]. Check if its mark have been illegally removed or not added")
    return
  end
  local oldIndex, total, color = mark[skillIndex][2], mark[skillIndex][3], mark[skillIndex][4]
  local newIndex, loopBack
  if add or (add == nil) then
    newIndex = oldIndex + 1
    if newIndex > total then
      loopBack = true
      newIndex = 1
    end
  else
    newIndex = oldIndex - 1
    if newIndex < 1 then
      newIndex = total
    end
  end
  mark[skillIndex][2] = newIndex
  room:setPlayerMark(player, "diy_switch", mark)
  if color then
    refreshSwitchMark(player, color, oldIndex, newIndex)
  end
  -- 触发“周始”
  if loopBack then
    room.logic:trigger(DIY.SkillSwitchLoopback, player, { skill = Fk.skills[skillName], who = player })
  end
end

--- 设置多项转换技的状态。也可用于新建多项转换技
---@param player ServerPlayer @ 目标角色
---@param skillName string @ 技能名
---@param index? integer @ 设置当前项数
---@param total? integer @ 转换技总项数
DIY.setSwitchState = function (player, skillName, index, total)
  local mark = player:getTableMark("diy_switch")
  local colors, oldIndex = {"red", "green", "yellow", "blue"}, nil
  local skillIndex -- 判断此转换技在该角色所有转换技的储存位置
  for i, v in ipairs(mark) do
    if v[1] == skillName then
      skillIndex = i
      break
    end
  end
  if skillIndex == nil then -- 找不到记录，则新建一个记录
    table.insert(mark, {skillName, index or 1, total or 3})
    skillIndex = #mark
    -- 为此转换技分配标记颜色
    local color = table.find(colors, function(c)
      return table.every(mark, function(v) return v[4] ~= c end)
    end)
    if color then
      mark[skillIndex][4] = color
    end
  else
    oldIndex = mark[skillIndex][2]
  end
  if index then
    mark[skillIndex][2] = index
  end
  if total then
    mark[skillIndex][3] = total
  end
  player.room:setPlayerMark(player, "diy_switch", mark)
  if mark[skillIndex][4] then
    refreshSwitchMark(player, mark[skillIndex][4], oldIndex, index)
  end
end


--- 清除一个多项转换技
---@param player ServerPlayer @ 目标角色
---@param skillName string @ 技能名
DIY.removeSwitchSkill = function (player, skillName)
  local mark = player:getTableMark("diy_switch")
  local color, index
  for i, v in ipairs(mark) do
    if v[1] == skillName then
      if v[4] then
        color = v[4]
        index = v[2]
      end
      table.remove(mark, i)
      break
    end
  end
  player.room:setPlayerMark(player, "diy_switch", mark)
  if color then
    refreshSwitchMark(player, color, index)
    -- 清除后可以把它分配的颜色转移给未分配颜色的技能，以后再说吧
  end
end


--- 获取多项转换技的当前项。若未记录，返回0
---@param player Player @ 目标角色
---@param skillName string @ 技能名
---@return integer
DIY.getSwitchState = function (player, skillName)
  local mark = player:getTableMark("diy_switch")
  for i, v in ipairs(mark) do
    if v[1] == skillName then
      return v[2]
    end
  end
  return 0
end



---------------------------------------------------------------------------------------------------
--- 实用类（不依赖3D概念，但官服从未应用过的函数）

--- 获得一张**实体牌**最后一次离开非处理区进入非角色区域时位于谁的区域
---@param room Room @ 房间
---@param card Card | integer @ **实体牌**或其ID
---@return ServerPlayer? @ 最后区域所有者，若无则表示此牌来自非角色区域，或从未如此离开过
DIY.getCardLastOwner = function(room, card)
  local logic = room.logic
  local cid = Card:getIdList(card)[1]
  local last_owner = room:getCardOwner(cid) ---@cast last_owner ServerPlayer
  if last_owner then return last_owner end
  logic:getEventsByRule(GameEvent.MoveCards, 1, function(e)
    for _, move in ipairs(e.data) do
      if move.to == nil then
        for _, info in ipairs(move.moveInfo) do
          if info.cardId == cid and info.fromArea ~= Card.Processing then
            last_owner = move.from
            return true
          end
        end
      end
    end
  end, 0)
  return last_owner
end








dofile 'packages/diy_utility/aux_events/ready_skill.lua'
dofile 'packages/diy_utility/aux_events/switch_skill.lua'


return DIY
